بر هوک useFormState در React مسلط شوید. راهنمایی جامع برای مدیریت ساده وضعیت فرم، اعتبارسنجی سمت سرور و بهبود تجربه کاربری با Server Actions.
هوک useFormState در React: نگاهی عمیق به مدیریت و اعتبارسنجی مدرن فرمها
فرمها سنگ بنای تعامل در وب هستند. از فرمهای تماس ساده گرفته تا فرمهای چندمرحلهای پیچیده، آنها برای دریافت ورودی کاربر و ارسال دادهها ضروری هستند. سالهاست که توسعهدهندگان React با مجموعهای از راهحلهای مدیریت وضعیت، از هوکهای ساده useState برای سناریوهای ابتدایی گرفته تا کتابخانههای قدرتمند شخص ثالث مانند Formik و React Hook Form برای نیازهای پیچیدهتر، سر و کار داشتهاند. در حالی که این ابزارها عالی هستند، React به طور مداوم در حال تکامل است تا ابزارهای اولیه قدرتمند و یکپارچهتری ارائه دهد.
با useFormState آشنا شوید، هوکی که در React 18 معرفی شد. useFormState که در ابتدا برای کار یکپارچه با React Server Actions طراحی شده بود، رویکردی ساده، قوی و بومی برای مدیریت وضعیت فرم ارائه میدهد، به خصوص هنگام کار با منطق و اعتبارسنجی سمت سرور. این هوک فرآیند نمایش بازخورد از سرور، مانند خطاهای اعتبارسنجی یا پیامهای موفقیت، را مستقیماً در رابط کاربری شما ساده میکند.
این راهنمای جامع شما را به یک بررسی عمیق از هوک useFormState میبرد. ما مفاهیم اصلی، پیادهسازیهای عملی، الگوهای پیشرفته و نحوه جایگیری آن در اکوسیستم گستردهتر توسعه مدرن React را بررسی خواهیم کرد. چه در حال ساخت برنامههایی با Next.js، Remix یا React خالص باشید، درک useFormState شما را به ابزاری قدرتمند برای ساخت فرمهای بهتر و مقاومتر مجهز میکند.
هوک `useFormState` چیست و چرا به آن نیاز داریم؟
در هسته خود، useFormState هوکی است که برای بهروزرسانی وضعیت بر اساس نتیجه یک اکشن فرم طراحی شده است. آن را به عنوان یک نسخه تخصصی از useReducer در نظر بگیرید که به طور خاص برای ارسال فرمها ساخته شده است. این هوک به زیبایی شکاف بین تعامل کاربر در سمت کلاینت و پردازش در سمت سرور را پر میکند.
قبل از useFormState، یک جریان ارسال فرم معمولی که شامل سرور میشد، ممکن بود به این شکل باشد:
- کاربر یک فرم را پر میکند.
- وضعیت سمت کلاینت (مثلاً با استفاده از
useState) مقادیر ورودی را ردیابی میکند. - هنگام ارسال، یک event handler (
onSubmit) از رفتار پیشفرض مرورگر جلوگیری میکند. - یک درخواست
fetchبه صورت دستی ساخته و به یک نقطه پایانی API سرور ارسال میشود. - وضعیتهای بارگذاری مدیریت میشوند (مثلاً
const [isLoading, setIsLoading] = useState(false)). - سرور درخواست را پردازش میکند، اعتبارسنجی را انجام میدهد و با پایگاه داده تعامل میکند.
- سرور یک پاسخ JSON برمیگرداند (مثلاً
{ success: false, errors: { email: 'Invalid format' } }). - کد سمت کلاینت این پاسخ را تجزیه کرده و یک متغیر وضعیت دیگر را برای نمایش خطاها یا پیامهای موفقیت بهروز میکند.
این فرآیند، هرچند کاربردی، شامل کد تکراری قابل توجهی برای مدیریت وضعیتهای بارگذاری، خطاها و چرخه درخواست/پاسخ است. useFormState، به ویژه هنگامی که با Server Actions همراه شود، این فرآیند را با ایجاد یک جریان اعلانیتر و یکپارچهتر به شدت ساده میکند.
مزایای اصلی استفاده از useFormState عبارتند از:
- یکپارچگی بینقص با سرور: این راهحل بومی برای مدیریت پاسخها از Server Actions است و اعتبارسنجی سمت سرور را به یک شهروند درجه یک در کامپوننت شما تبدیل میکند.
- مدیریت وضعیت سادهشده: این هوک منطق بهروزرسانیهای وضعیت فرم را متمرکز میکند و نیاز به چندین هوک
useStateبرای دادهها، خطاها و وضعیت ارسال را کاهش میدهد. - بهبود تدریجی (Progressive Enhancement): فرمهای ساخته شده با
useFormStateو Server Actions میتوانند حتی اگر جاوا اسکریپت در کلاینت غیرفعال باشد کار کنند، زیرا بر پایه ارسال فرمهای استاندارد HTML ساخته شدهاند. - تجربه کاربری بهبودیافته: این هوک ارائه بازخورد فوری و متنی به کاربر، مانند خطاهای اعتبارسنجی درونخطی یا پیامهای موفقیت، را بلافاصله پس از ارسال فرم آسانتر میکند.
درک امضای هوک `useFormState`
برای تسلط بر این هوک، ابتدا بیایید امضا و مقادیر بازگشتی آن را بررسی کنیم. این سادهتر از آن چیزی است که در ابتدا به نظر میرسد.
const [state, formAction] = useFormState(action, initialState);
پارامترها:
action: این تابعی است که هنگام ارسال فرم اجرا میشود. این تابع دو آرگومان دریافت میکند: وضعیت قبلی فرم و دادههای ارسالی فرم. انتظار میرود که وضعیت جدید را برگرداند. این معمولاً یک Server Action است، اما میتواند هر تابعی باشد.initialState: این مقداری است که میخواهید وضعیت فرم در ابتدا، قبل از هرگونه ارسال، داشته باشد. این میتواند هر مقدار قابل سریالسازی (رشته، عدد، شیء و غیره) باشد.
مقادیر بازگشتی:
useFormState یک آرایه با دقیقاً دو عنصر برمیگرداند:
state: وضعیت فعلی فرم. در اولین رندر، این مقدار همانinitialStateخواهد بود که شما ارائه دادهاید. پس از ارسال فرم، این مقدار همان چیزی خواهد بود که تابعactionشما برگردانده است. این وضعیت چیزی است که شما برای رندر کردن بازخورد رابط کاربری، مانند پیامهای خطا، استفاده میکنید.formAction: یک تابع اکشن جدید که شما آن را به پراپactionعنصر<form>خود میدهید. هنگامی که این اکشن (با ارسال فرم) فعال میشود، React تابعactionاصلی شما را با وضعیت قبلی و دادههای فرم فراخوانی کرده و سپسstateرا با نتیجه بهروز میکند.
این الگو ممکن است برای شما آشنا باشد اگر از useReducer استفاده کرده باشید. تابع action مانند یک reducer است، initialState همان وضعیت اولیه است و React مدیریت dispatch را برای شما هنگام ارسال فرم انجام میدهد.
یک مثال عملی اولیه: فرم اشتراک ساده
بیایید یک فرم اشتراک خبرنامه ساده بسازیم تا useFormState را در عمل ببینیم. ما یک ورودی ایمیل و یک دکمه ارسال خواهیم داشت. اکشن سرور اعتبارسنجی اولیهای را برای بررسی اینکه آیا ایمیل ارائه شده و آیا فرمت آن معتبر است، انجام میدهد.
ابتدا، بیایید اکشن سرور خود را تعریف کنیم. اگر از Next.js استفاده میکنید، میتوانید این را در همان فایل کامپوننت خود با افزودن دستور 'use server'; در بالای تابع قرار دهید.
// In actions.js or at the top of your component file with 'use server'
export async function subscribe(previousState, formData) {
const email = formData.get('email');
if (!email) {
return { message: 'Email is required.' };
}
// A simple regex for demonstration purposes
if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(email)) {
return { message: 'Please enter a valid email address.' };
}
// Here you would typically save the email to a database
console.log(`Subscribing with email: ${email}`);
// Simulate a delay
await new Promise(res => setTimeout(res, 1000));
return { message: 'Thank you for subscribing!' };
}
حالا، بیایید کامپوننت کلاینت را بسازیم که از این اکشن با useFormState استفاده میکند.
'use client';
import { useFormState } from 'react-dom';
import { subscribe } from './actions';
const initialState = {
message: null,
};
export function SubscriptionForm() {
const [state, formAction] = useFormState(subscribe, initialState);
return (
<form action={formAction}>
<h3>Subscribe to Our Newsletter</h3>
<div>
<label htmlFor="email">Email Address</label>
<input type="email" id="email" name="email" required />
</div>
<button type="submit">Subscribe</button>
{state?.message && <p>{state.message}</p>}
</form>
);
}
بیایید بررسی کنیم چه اتفاقی میافتد:
- ما
useFormStateرا ازreact-domوارد میکنیم (توجه: نه ازreact). - ما یک شیء
initialStateتعریف میکنیم. این تضمین میکند که متغیرstateما از اولین رندر یک شکل ثابت داشته باشد. - ما
useFormState(subscribe, initialState)را فراخوانی میکنیم. این وضعیت کامپوننت ما را به اکشن سرورsubscribeمتصل میکند. formActionبازگشتی به پراپactionعنصر<form>داده میشود. این همان اتصال جادویی است.- ما پیام را از شیء
stateخود به صورت شرطی رندر میکنیم. در اولین رندر،state.messageبرابرnullاست، بنابراین چیزی نمایش داده نمیشود. - هنگامی که کاربر فرم را ارسال میکند، React
formActionرا فراخوانی میکند. این اکشن سرورsubscribeما را فعال میکند. تابعsubscribepreviousState(که در ابتدا همانinitialStateما است) وformDataرا دریافت میکند. - اکشن سرور منطق خود را اجرا کرده و یک شیء وضعیت جدید برمیگرداند (مثلاً
{ message: 'Email is required.' }). - React این وضعیت جدید را دریافت کرده و کامپوننت
SubscriptionFormرا دوباره رندر میکند. متغیرstateاکنون شیء جدید را در خود دارد و پاراگراف شرطی ما پیام خطا یا موفقیت را نمایش میدهد.
این فوقالعاده قدرتمند است. ما یک حلقه کامل اعتبارسنجی کلاینت-سرور را با حداقل کد تکراری مدیریت وضعیت سمت کلاینت پیادهسازی کردهایم.
بهبود تجربه کاربری (UX) با `useFormStatus`
فرم ما کار میکند، اما تجربه کاربری میتواند بهتر باشد. وقتی کاربر روی «Subscribe» کلیک میکند، دکمه فعال باقی میماند و هیچ نشانه بصری از اینکه چیزی در حال رخ دادن است وجود ندارد تا زمانی که سرور پاسخ دهد. اینجاست که هوک useFormStatus به کار میآید.
هوک useFormStatus اطلاعات وضعیت در مورد آخرین ارسال فرم را ارائه میدهد. نکته بسیار مهم این است که این هوک باید در کامپوننتی استفاده شود که فرزند عنصر <form> است. اگر در همان کامپوننتی که فرم را رندر میکند فراخوانی شود، کار نخواهد کرد.
بیایید یک کامپوننت جداگانه SubmitButton ایجاد کنیم.
'use client';
import { useFormStatus } from 'react-dom';
export function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Subscribing...' : 'Subscribe'}
</button>
);
}
حالا میتوانیم SubscriptionForm خود را برای استفاده از این کامپوننت جدید بهروز کنیم:
// ... imports
import { SubmitButton } from './SubmitButton';
// ... initialState and other code
export function SubscriptionForm() {
const [state, formAction] = useFormState(subscribe, initialState);
return (
<form action={formAction}>
{/* ... form inputs ... */}
<SubmitButton /> {/* Replace the old button */}
{state?.message && <p>{state.message}</p>}
</form>
);
}
با این تغییر، هنگامی که فرم ارسال میشود، مقدار pending از useFormStatus برابر true میشود. کامپوننت SubmitButton ما دوباره رندر شده، دکمه را غیرفعال کرده و متن آن را به «Subscribing...» تغییر میدهد. هنگامی که اکشن سرور کامل شد و useFormState وضعیت را بهروز کرد، فرم دیگر در حالت pending نیست و دکمه به حالت اولیه خود بازمیگردد. این بازخورد ضروری را به کاربر ارائه میدهد و از ارسالهای تکراری جلوگیری میکند.
اعتبارسنجی پیشرفته با وضعیتهای خطای ساختاریافته و Zod
یک پیام رشتهای برای فرمهای ساده کافی است، اما برنامههای دنیای واقعی اغلب به خطاهای اعتبارسنجی برای هر فیلد نیاز دارند. ما میتوانیم به راحتی با برگرداندن یک شیء وضعیت ساختاریافتهتر از اکشن سرور خود به این هدف برسیم.
بیایید اکشن خود را طوری ارتقا دهیم که یک شیء با کلید errors برگرداند که خود حاوی پیامهایی برای فیلدهای خاص است. این همچنین فرصتی عالی برای معرفی یک کتابخانه اعتبارسنجی اسکیمامانند Zod برای منطق اعتبارسنجی قویتر و قابل نگهداریتر است.
مرحله ۱: نصب Zod
npm install zod
مرحله ۲: بهروزرسانی اکشن سرور
ما یک اسکیمای Zod برای تعریف شکل مورد انتظار و قوانین اعتبارسنجی برای دادههای فرم خود ایجاد خواهیم کرد. سپس، از schema.safeParse() برای اعتبارسنجی formData ورودی استفاده خواهیم کرد.
'use server';
import { z } from 'zod';
// Define the schema for our form
const contactSchema = z.object({
name: z.string().min(2, { message: 'Name must be at least 2 characters.' }),
email: z.string().email({ message: 'Invalid email address.' }),
message: z.string().min(10, { message: 'Message must be at least 10 characters.' }),
});
export async function submitContactForm(previousState, formData) {
const validatedFields = contactSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
});
// If validation fails, return the errors
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: 'Validation failed. Please check your inputs.',
};
}
// If validation succeeds, process the data
// For example, send an email or save to a database
console.log('Success!', validatedFields.data);
// ... processing logic ...
// Return a success state
return {
errors: {},
message: 'Thank you for your message! We will get back to you soon.',
};
}
توجه کنید که چگونه از validatedFields.error.flatten().fieldErrors استفاده میکنیم. این یک ابزار مفید Zod است که شیء خطا را به یک ساختار قابل استفادهتر تبدیل میکند، مانند: { name: ['Name must be at least 2 characters.'], message: ['Message is too short'] }.
مرحله ۳: بهروزرسانی کامپوننت کلاینت
اکنون، کامپوننت فرم خود را برای مدیریت این وضعیت خطای ساختاریافته بهروز خواهیم کرد.
'use client';
import { useFormState } from 'react-dom';
import { submitContactForm } from './actions';
import { SubmitButton } from './SubmitButton'; // Assuming we have a submit button
const initialState = {
message: null,
errors: {},
};
export function ContactForm() {
const [state, formAction] = useFormState(submitContactForm, initialState);
return (
<form action={formAction}>
<h2>Contact Us</h2>
<div>
<label htmlFor="name">Name</label>
<input type="text" id="name" name="name" />
{state.errors?.name && (
<p className="error">{state.errors.name[0]}</p>
)}
</div>
<div>
<label htmlFor="email">Email</label>
<input type="email" id="email" name="email" />
{state.errors?.email && (
<p className="error">{state.errors.email[0]}</p>
)}
</div>
<div>
<label htmlFor="message">Message</label>
<textarea id="message" name="message" />
{state.errors?.message && (
<p className="error">{state.errors.message[0]}</p>
)}
</div>
<SubmitButton />
{state.message && <p className="form-status">{state.message}</p>}
</form>
);
}
این الگو فوقالعاده مقیاسپذیر و قوی است. اکشن سرور شما به تنها منبع حقیقت برای منطق اعتبارسنجی تبدیل میشود و Zod یک روش اعلانی و ایمن از نظر نوع (type-safe) برای تعریف آن قوانین ارائه میدهد. کامپوننت کلاینت به سادگی مصرفکننده وضعیتی میشود که توسط useFormState ارائه شده و خطاها را در جای مناسب نمایش میدهد. این جداسازی مسئولیتها کد را تمیزتر، تستپذیرتر و امنتر میکند، زیرا اعتبارسنجی همیشه در سرور اعمال میشود.
مقایسه `useFormState` با دیگر راهحلهای مدیریت فرم
با هر ابزار جدیدی این سوال پیش میآید: «چه زمانی باید از این به جای آنچه قبلاً میدانستم استفاده کنم؟» بیایید useFormState را با سایر رویکردهای رایج مقایسه کنیم.
مقایسه `useFormState` با `useState`
- `useState` برای فرمهای ساده و فقط-کلاینت یا زمانی که نیاز به انجام تعاملات پیچیده و بلادرنگ سمت کلاینت (مانند اعتبارسنجی زنده هنگام تایپ کاربر) قبل از ارسال دارید، عالی است. این به شما کنترل مستقیم و دقیق میدهد.
- `useFormState` زمانی برتری دارد که وضعیت فرم عمدتاً توسط پاسخ سرور تعیین میشود. این برای چرخه درخواست/پاسخ ارسال فرم طراحی شده و انتخاب اصلی هنگام استفاده از Server Actions است. این هوک نیاز به مدیریت دستی فراخوانیهای fetch، وضعیتهای بارگذاری و تجزیه پاسخ را از بین میبرد.
مقایسه `useFormState` با کتابخانههای شخص ثالث (React Hook Form, Formik)
کتابخانههایی مانند React Hook Form و Formik راهحلهای بالغ و پر از ویژگی هستند که مجموعه جامعی از ابزارها را برای مدیریت فرم ارائه میدهند. آنها فراهم میکنند:
- اعتبارسنجی پیشرفته سمت کلاینت (اغلب با یکپارچگی با اسکیما برای Zod، Yup و غیره).
- مدیریت وضعیت پیچیده برای فیلدهای تودرتو، آرایههای فیلد و موارد دیگر.
- بهینهسازیهای عملکرد (مثلاً محدود کردن رندرهای مجدد فقط به ورودیهایی که تغییر میکنند).
- ابزارهای کمکی برای کامپوننتهای کنترلشده و یکپارچگی با کتابخانههای UI.
پس، چه زمانی کدام را انتخاب میکنید؟
useFormStateرا انتخاب کنید زمانی که:- شما از React Server Actions استفاده میکنید و یک راهحل بومی و یکپارچه میخواهید.
- منبع اصلی حقیقت اعتبارسنجی شما سرور است.
- شما برای بهبود تدریجی ارزش قائل هستید و میخواهید فرمهایتان بدون جاوا اسکریپت کار کنند.
- منطق فرم شما نسبتاً ساده و متمرکز بر چرخه ارسال/پاسخ است.
- یک کتابخانه شخص ثالث را انتخاب کنید زمانی که:
- شما به اعتبارسنجی گسترده و پیچیده سمت کلاینت با بازخورد فوری نیاز دارید (مثلاً اعتبارسنجی در رویداد on blur یا on change).
- شما فرمهای بسیار پویا دارید (مثلاً افزودن/حذف فیلدها، منطق شرطی).
- شما از فریمورکی با Server Actions استفاده نمیکنید و لایه ارتباطی کلاینت-سرور خود را با APIهای REST یا GraphQL میسازید.
- شما به کنترل دقیق بر عملکرد و رندرهای مجدد در فرمهای بسیار بزرگ نیاز دارید.
همچنین مهم است توجه داشته باشید که اینها متقابلاً انحصاری نیستند. شما میتوانید از React Hook Form برای مدیریت وضعیت و اعتبارسنجی سمت کلاینت فرم خود استفاده کنید و سپس از handler ارسال آن برای فراخوانی یک Server Action استفاده کنید. با این حال، برای بسیاری از موارد استفاده رایج، ترکیب useFormState و Server Actions راهحل سادهتر و زیباتری ارائه میدهد.
بهترین شیوهها و اشتباهات رایج
برای بهرهبرداری حداکثری از useFormState، بهترین شیوههای زیر را در نظر بگیرید:
- اکشنها را متمرکز نگه دارید: تابع اکشن فرم شما باید مسئول یک چیز باشد: پردازش ارسال فرم. این شامل اعتبارسنجی، تغییر دادهها (ذخیره در پایگاه داده) و برگرداندن وضعیت جدید است. از عوارض جانبی که به نتیجه فرم بیربط هستند، خودداری کنید.
- یک شکل وضعیت ثابت تعریف کنید: همیشه با یک
initialStateخوب تعریف شده شروع کنید و اطمینان حاصل کنید که اکشن شما همیشه یک شیء با همان شکل را برمیگرداند، حتی در صورت موفقیت. این از خطاهای زمان اجرا در کلاینت هنگام تلاش برای دسترسی به ویژگیهایی مانندstate.errorsجلوگیری میکند. - بهبود تدریجی را بپذیرید: به یاد داشته باشید که Server Actions بدون جاوا اسکریپت سمت کلاینت کار میکنند. رابط کاربری خود را طوری طراحی کنید که هر دو سناریو را به خوبی مدیریت کند. به عنوان مثال، اطمینان حاصل کنید که پیامهای اعتبارسنجی رندر شده در سرور واضح هستند، زیرا کاربر بدون JS از مزیت وضعیت دکمه غیرفعال بهرهمند نخواهد شد.
- نگرانیهای UI را جدا کنید: از کامپوننتهایی مانند
SubmitButtonما برای کپسوله کردن UI وابسته به وضعیت استفاده کنید. این کار کامپوننت اصلی فرم شما را تمیزتر نگه میدارد و به این قانون احترام میگذارد کهuseFormStatusباید در یک کامپوننت فرزند استفاده شود. - دسترسیپذیری را فراموش نکنید: هنگام نمایش خطاها، از ویژگیهای ARIA مانند
aria-invalidدر فیلدهای ورودی خود استفاده کنید و پیامهای خطا را با ورودیهای مربوطه خود با استفاده ازaria-describedbyمرتبط کنید تا اطمینان حاصل شود که فرمهای شما برای کاربران صفحهخوان قابل دسترسی هستند.
اشتباه رایج: استفاده از useFormStatus در همان کامپوننت
یک اشتباه مکرر، فراخوانی useFormStatus در همان کامپوننتی است که تگ <form> را رندر میکند. این کار نخواهد کرد زیرا هوک برای دسترسی به وضعیت فرم، باید در داخل زمینه آن باشد. همیشه بخشی از UI خود را که به وضعیت نیاز دارد (مانند یک دکمه) به کامپوننت فرزند خود استخراج کنید.
نتیجهگیری
هوک useFormState، در هماهنگی با Server Actions، نشاندهنده یک تکامل قابل توجه در نحوه مدیریت فرمها در React است. این هوک توسعهدهندگان را به سمت یک مدل اعتبارسنجی قویتر و سرور-محور سوق میدهد در حالی که مدیریت وضعیت سمت کلاینت را ساده میکند. با انتزاعی کردن پیچیدگیهای چرخه حیات ارسال، به ما امکان میدهد تا بر روی آنچه بیشترین اهمیت را دارد تمرکز کنیم: تعریف منطق تجاری خود و ساخت یک تجربه کاربری یکپارچه.
در حالی که ممکن است برای هر مورد استفادهای جایگزین کتابخانههای جامع شخص ثالث نشود، useFormState یک پایه قدرتمند، بومی و با بهبود تدریجی برای اکثریت قریب به اتفاق فرمها در برنامههای وب مدرن فراهم میکند. با تسلط بر الگوهای آن و درک جایگاه آن در اکوسیستم React، میتوانید فرمهای مقاومتر، قابل نگهداریتر و کاربرپسندتری با کد کمتر و وضوح بیشتر بسازید.